In this chapter, we present a sequence of six increasingly more versatile versions of a "Hello World" view object. Views are the central abstraction in Oberon/F. Everything revolves around views: commands operate on views, windows display views, views can be internalized and externalized as well as displayed, and views may be embedded into other views.
The first version of our "Hello World" view is about the simplest possible view implementation. Its major components are the definition of a Views.View extension, the implementation of a Views.Restore procedure which draws the contents of the view, and a command procedure which allocates, initializes, and opens the view in a window:
MODULE TutorialEx11;
(* Open a simple view which displays the string "Hello World" *)
To execute this program, invoke the following command:
"TutorialEx11.Deposit; Open"
This simple program is completely functional already. The procedure TutorialEx11.Deposit puts a newly allocated view into a queue. The pseudo-procedure Open in the command string above removes a view from the queue, and opens it into a window. Alternatively, the view could be pasted:
"TutorialEx11.Deposit; PasteView"
The pseudo-procedure PasteView removes a view from the queue, but pastes it into the focus view.
But let us return to the first command, which opened a window. The window's contents can be saved in a file, and this file can be opened again using the standard Open... menu entry. Printing the view is already possible as well.
Picture 4a Result of "TutorialEx11.Deposit; Open"
Every view performs its output operations and mouse/keyboard polling via a Views.Frame object. In the above example, TutorialEx11.View.Restore uses its frame parameter f to draw a string. In analogy to the file system, a frame can be regarded as a mapper object, in this case for both input and output simultaneously. A frame embodies coordinate transformations and clipping facilities. An important property of Oberon/F frames can be stated here:
In each window, there is exactly one frame for every (at least partially) visible view.
The next version of our view implementation does not display the constant string "Hello World", instead the user may type in an arbitrary string (the string's length is limited to 255 characters, but no error handling is performed in these demonstration programs).
MODULE TutorialEx12;
(* Same as TutorialEx11, but an arbitrary string is displayed which can be typed in *)
IMPORT Fonts, Ports, Views, Controllers;
CONST d = 20 * Ports.point;
TYPE
View = POINTER TO ViewDesc;
ViewDesc = RECORD (Views.ViewDesc)
i: INTEGER; (* position of next free slot in string *)
PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message;
VAR focus: Views.View);
BEGIN
WITH msg: Controllers.PollOpsMsg DO
msg.valid := {Controllers.pasteChar} (* tell that v accepts typing *)
| msg: Controllers.EditMsg DO
IF msg.op = Controllers.pasteChar THEN (* accept typing *)
v.s[v.i] := msg.char; INC(v.i); v.s[v.i] := 0X; (* append character to string *)
Views.Update(v, Views.keepFrames) (* restore v in any frame that displays it *)
END
ELSE (* ignore other messages *)
END
END HandleCtrlMsg;
PROCEDURE Deposit*;
VAR v: View;
BEGIN
NEW(v); v.Init();
v.s := ""; v.i := 0;
Views.Deposit(v)
END Deposit;
END TutorialEx12.
A typed character is sent to a view in the form of a controller message record (Controllers.Message), to be handled by the view's HandleCtrlMsg procedure. This procedure reacts by inserting the character contained in the message into a string field s of the view record. Afterwards, it causes the view to be restored, i.e. wherever the view is visible on the display it is redrawn in its new state, displaying the string that has become one character longer.
Note that the HandleCtrlMsg is called with a Controllers.EditMsg as actual parameter when a character was typed in. However, a Controllers.PollOpsMsg may be sent to the view as well. Such a message inquires about the operations that a view supports. In our case, typing ("pasting") a character is the only operation supported.
In the above example, a view contains no constant state (the "Hello World" string) anymore, but rather a variable state (the view's string field s). Thus this state needs to be saved in the file when the window's contents are saved to disk. For this purpose, a view provides two procedures, called Internalize and Externalize, whose uses are shown in the next iteration of our example program:
MODULE TutorialEx13;
(* Same as TutorialEx12, but the view's string can be stored and copied *)
IMPORT Fonts, Ports, Stores, Views, Controllers;
CONST d = 20 * Ports.point;
TYPE
View = POINTER TO ViewDesc;
ViewDesc = RECORD (Views.ViewDesc)
i: INTEGER; (* position of next free slot in string *)
PROCEDURE (v: View) HandleCtrlMsg (f: Views.Frame; VAR msg: Controllers.Message; VAR focus: Views.View);
BEGIN
WITH msg: Controllers.PollOpsMsg DO
msg.valid := {Controllers.pasteChar} (* tell that v accepts typing *)
| msg: Controllers.EditMsg DO
IF msg.op = Controllers.pasteChar THEN (* accept typing *)
Views.BeginModification(v); (* mark view's document as dirty *)
v.s[v.i] := msg.char; INC(v.i); v.s[v.i] := 0X; (* append character to string *)
Views.EndModification(v);
Views.Update(v, Views.keepFrames) (* restore v in any frame that displays it *)
END
ELSE (* ignore other messages *)
END
END HandleCtrlMsg;
PROCEDURE Deposit*;
VAR v: View;
BEGIN
NEW(v); v.Init();
v.s := ""; v.i := 0;
Views.Deposit(v)
END Deposit;
END TutorialEx13.
A few comments about View.Internalize and View.Externalize are in order here: First, the two procedures have a reader, respectively a writer, as variable parameters. As mentioned earlier in the files examples, these file mappers are set up by Oberon/F itself, a view simply uses them.
Second, these procedures must call their base procedures ("super calls") such that the base types get the opportunity to read/write their own data.
Third, View.Internalize must read exactly the same (amount of) data that View.Externalize has written.
Fourth, a user interface typically defines some visual distinction for documents that contain modified views, i.e. for "dirty" documents. Or it may require that when the user closes a dirty document, he should be asked whether to save it or not. In order to make this possible, a view must tell when it has been modified. This is done by the Views.BeginModification /Views.EndModification calls.
In addition to View.Internalize and View.Externalize, the above view implementation also implements a CopyFrom procedure. Such a procedure should copy the view's contents, given a source view of the same type. CopyFrom should have the same effect as if the source's contents were externalized on a temporary file, and then internalized again by the destination view.
Basically, TutorialEx13 has shown most of what is involved in implementing a simple view class. Such simple views are often sufficient, consequently it is important that they are easy to implement. However, there are cases where a more advanced view design is in order. In particular, if a window normally can only display a small part of a view's contents, multi-view editing should be supported.
Multi-view editing means that there may be several views showing the same data. The typical application of this feature is to have two or more windows displaying the same document, each of these windows showing a different part of the document, in its own view. Thus, if a view's data has been changed, it and all the other affected views must be notified of the change, such that they can update the display accordingly.
The following sample program, which is the same as the previous one except that it supports multi-view editing, is roughly twice as long. This indicates that the design and implementation of such views is quite a bit more involved than that of simple views. It is a major design decision whether this additional complexity is warranted by its increased convenience.
MODULE TutorialEx14;
(* Same as TutorialEx13, but uses a separate model for the string *)
Multi-view editing is realized by factoring out the view's data into a separate data structure, called a model. This model is shared between all its views, i.e. each such view contains a pointer to the model:
Picture 4b Two Views on one Model
A model is, like a view, an extension of a Stores.Store, which is the base type for all extensible and persistent objects. Stores define the Internalize and Externalize procedures we've already met for a view. In TutorialEx14, the model stores the string and its length, while the view stores the whole model! For this purpose, a Stores.Writer provides a WriteStore and a Stores.Reader provides a ReadStore procedure.
As a consequence of this separation, copying the string is now done by the model, while the CopyModelFrom of the view copies the model itself. A model, like any store, has a generic Clone procedure, which returns a new object of the same type as the object being cloned. However, the new object is neither copied nor even initialized!
The TutorialEx14 example introduces an auxiliary procedure View.InitModel, which assigns a model to a non-initialized view.
For views which are capable of multi-view editing, copying can mean several things. CopyFrom as implemented in the previous example copies the view-specific state, i.e. the state which is private to each view, and not shared like the model. CopyModelFrom copies the shared state, usually this is just the model. It can do this in two different ways: either it copies only the pointer to the model, this is called a shallow copy. Or, it clones the model, copies its contents, and assigns the clone. This is called a deep copy. After a deep copy, the source and its copy become independent, since they don't share any state.
View.ThisModel returns the model of the view. Its default implementation returns NIL, which should be overridden in views that contain models. This procedure is used by the framework to find all views that display a given model, in order to implement model broadcasts, as described in the next paragraph.
When a model changes, all views displaying it must be updated. For this purpose, a model broadcast is generated: A model broadcast notifies all views which display a particular model about a model modification that has happened. This gives each view the opportunity to restore its contents on the screen. A model broadcast is generated in the View.HandleCtrlMsg procedure by calling Models.Broadcast. The message is received by every view which contains the correct model, via its View.HandleModelMsg procedure.
This indirection becomes important if different types of views display the same model, e.g. a tabular view and a graphical view on a spreadsheet model; and if instead of restoring the whole view - as has been done in our examples - only the necessary parts of the views are restored. These necessary parts may be completely different for different types of views.
A very sophisticated model may be able to contain ("embed") arbitrary views as part of its contents. Such a container view implements recursive view embedding. For example, a text view may display a text model which contains not only characters (its intrinsic contents), but also graphical or other views flowing along in the text stream.
The combination of multi-view editing and recursive view embedding can lead to the following situation, where two text views show the same text model, which contains a graphics view. Each text view lives in its own window and thus has its own frame. The graphics view is unique however, since it is embedded in the text model, which is shared by both views. Nevertheless the graphics can be visible in both text views simultaneously, and thus there can be two frames for this one view:
Picture 4c Two Frames on one View
As a consequence, one and the same view may be visible in several places on the screen simultaneously. In this case, this means that when the view has changed, several places must be updated: the view must restore the necessary area once for every frame on this view. As a consequence, a notification mechanism must exist which lets the view update each of its frames. This is done in a similar way as the notification mechanism for model changes: with a view broadcast. Fortunately, there is a standard update mechanism in the framework which automatically handles this second broadcast level (see below).
We now can summarize the typical events that occur when a user interacts with a view:
Controller message handling:
1) Some controller message is sent to the focus view.
2) The focus view interprets the message and changes its model accordingly.
3) The model broadcasts a model message, describing the change that has been performed.
Model message handling:
4) Every view on this model receives the model message.
5) It determines how its display should change because of this model modification.
6) It broadcasts a view message, describing how its display should change.
View message handling:
7) The view receives the view notification message once for every frame on this view.
8) Every time, the view redraws the frame contents according to the view message.
This mechanism is fundamental to Oberon/F. If you have understood it, you have mastered the most complicated part of a view implementation. Interpreting the various controller messages, which are defined in module Controllers, is more a matter of diligence than of understanding difficult new concepts. Moreover, you only need to interpret the controller messages in which you are interested, because messages which are not relevant can simply be ignored.
If the necessary display change can be realized through restoration of some area, steps 6, 7, and 8 are handled by the framework: the view only calls Views.Update if a whole view should be updated, or Views.UpdateIn if some rectangular area should be updated. Calls of these update procedures cause a lazy update, i.e. the framework adds up the region to be updated, and causes a restore of this region when the current command has terminated.
A view programmer needs to use view messages only if no complete restore is desired. This is the case only for marks like selection, focus, or caret marks. These marks can sometimes be handled more efficiently because they are involutory: applied twice, they have no effect. Thus marks are often switched on and off through custom view messages, not through the slower update mechanism.
Now that we have seen the crucial ingredients of a view implementation, several less central features can be presented. The next one is a variation of the above program, in which a controller message is not interpreted directly. Instead, an operation object is created and then executed. An operation provides a do / undo / redo capability, as shown in the program below:
MODULE TutorialEx15;
(* Same as TutorialEx14, but generate undoable operations for character insertion *)
As a last version of our sample views, we modify the previous variant by exporting the Model and View types, and by separating interface and implementation of these two types. In order to do the latter, so-called directory objects are introduced, which generate (hidden) default implementations of abstract data types. The way that operations are handled is modified as well.
Real Oberon/F subsystems would additionally split the module into two modules, one for the model and one for the view. A third module with commands might be introduced, if the number and complexity of commands warrant it (e.g. TextModels, TextViews, and TextCmds). Even more complicated views would further split views into views and controllers, each in its own module. However, these are advanced topics not covered in this documentation.
MODULE TutorialEx16;
(* Same as TutorialEx15, but interfaces and implementations separated, and operation directly in Insert procedure *)
The separation of a type's definition from its implementation is recommended in the design of new Oberon/F subsystems. However, simple view types which won't become publicly available or which are not meant to be extended can certainly dispense with this additional effort.